Ana içeriğe geç

Sahip Kontrolleri

Özet

  • Sahip kontrolleri, hesapların beklenen program tarafından sahiplenildiğini sağlar. Sahip kontrolleri olmadan, başkaca programlar tarafından sahiplenilen hesaplar bir talimat işleyicisinde kullanılabilir.
  • Anchor program hesap türleri Owner özelliğini uygular, bu da Account'nin program sahipliğini otomatik olarak doğrulamasını sağlar.
  • Ayrıca, Anchor'ın #[account(owner = )] kısıtlamasını kullanarak, mevcut programdan farklı bir hesap sahibini tanımlayabilirsiniz.
  • Yerel Rust kullanarak bir sahip kontrolü uygulamak için, hesabın sahibinin beklenen program kimliği ile eşleştiğini doğrulayın.
if ctx.accounts.account.owner != ctx.program_id {
return Err(ProgramError::IncorrectProgramId.into());
}

Ders

Sahip kontrolleri, bir talimat işleyicisine geçirilen bir hesabın beklenen program tarafından sahiplenildiğini doğrulamak için kullanılır; bu, farklı programlardan hesapların istismar edilmesini önler.

AccountInfo yapısı, hesap sahibi olan owner dahil olmak üzere birkaç alan içerir. Sahip kontrolleri, AccountInfo içindeki bu owner alanının beklenen program kimliği ile eşleştiğinden emin olur.

/// Hesap bilgileri
#[derive(Clone)]
pub struct AccountInfo<'a> {
/// Hesabın genel anahtarı
pub key: &'a Pubkey,
/// İşlem bu hesabın genel anahtarıyla mı imzalandı?
pub is_signer: bool,
/// Hesap yazılabilir mi?
pub is_writable: bool,
/// Hesapta bulunan lamportlar. Programlar tarafından değiştirilebilir.
pub lamports: Rc<RefCell<&'a mut u64>>,
/// Bu hesapta tutulan veri. Programlar tarafından değiştirilebilir.
pub data: Rc<RefCell<&'a mut [u8]>>,
/// Bu hesabı sahiplenen program
pub owner: &'a Pubkey,
/// Bu hesabın verileri, bir yüklenmiş program içerir (şimdi salt okunur)
pub executable: bool,
/// Bu hesabın bir sonraki kira borcunu ödeyeceği dönem
pub rent_epoch: Epoch,
}

Eksik sahip kontrolü

Aşağıdaki örnekte, admin_instruction bir admin hesabına kısıtlanması amaçlanmıştır; bu admin_config hesabında saklanır. Ancak, programın admin_config hesabını sahiplenip sahiplenmediğini kontrol etmekte başarısız olur. Bu kontrol olmadan, bir saldırgan hesabı sahteleştirerek yaptıramaz.

use anchor_lang::prelude::*;

declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws");

#[program]
pub mod owner_check {
use super::*;
...

pub fn admin_instruction(ctx: Context<Unchecked>) -> Result<()> {
let account_data = ctx.accounts.admin_config.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
let account_state = AdminConfig::try_deserialize(&mut account_data_slice)?;

if account_state.admin != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidArgument.into());
}
msg!("Admin: {}", account_state.admin.to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct Unchecked<'info> {
/// CHECK: Bu hesap Anchor tarafından kontrol edilmeyecek
admin_config: UncheckedAccount<'info>,
admin: Signer<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}

Sahip kontrolü ekleme

Bu sorunu yerel Rust kullanarak çözmek için, owner alanını program kimliği ile karşılaştırın:

if ctx.accounts.admin_config.owner != ctx.program_id {
return Err(ProgramError::IncorrectProgramId.into());
}

Bir owner kontrolü eklemek, diğer programlardan gelen hesapların talimat işleyicisine geçirilmesini engeller.

use anchor_lang::prelude::*;

declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws");

#[program]
pub mod owner_check {
use super::*;
...
pub fn admin_instruction(ctx: Context<Unchecked>) -> Result<()> {
if ctx.accounts.admin_config.owner != ctx.program_id {
return Err(ProgramError::IncorrectProgramId.into());
}

let account_data = ctx.accounts.admin_config.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
let account_state = AdminConfig::try_deserialize(&mut account_data_slice)?;

if account_state.admin != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidArgument.into());
}
msg!("Admin: {}", account_state.admin.to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct Unchecked<'info> {
/// CHECK: Bu hesap Anchor tarafından kontrol edilmeyecek
admin_config: UncheckedAccount<'info>,
admin: Signer<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}

Anchor'ın Account'sini kullanın

Anchor, AccountInfo'yı saran Account türü ile sahip kontrollerini basit hale getirir ve otomatik olarak sahipliğin doğrulanmasını sağlar.

Aşağıdaki örnekte, Account, admin_config hesabını doğrularken, has_one kısıtlaması, admin hesabının admin_config içindeki admin alanı ile eşleştiğini kontrol eder.

use anchor_lang::prelude::*;

declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws");

#[program]
pub mod owner_check {
use super::*;
...
pub fn admin_instruction(ctx: Context<Checked>) -> Result<()> {
msg!("Admin: {}", ctx.accounts.admin_config.admin.to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct Checked<'info> {
#[account(
has_one = admin,
)]
admin_config: Account<'info, AdminConfig>,
admin: Signer<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}

Anchor'ın #[account(owner = )] kısıtlamasını kullanın

Account türüne ek olarak, bir hesabın sahibi olması gereken programı belirtmek için Anchor'ın owner kısıtlamasını kullanabilirsiniz; bu, çağrılan programdan farklı olduğunda özeldir. Bu, bir talimat işleyicisinin hesaptan, başka bir program tarafından oluşturulmuş bir PDA beklediğinde özellikle yararlıdır. seeds ve bump kısıtlamaları ile birlikte owner kullanarak, hesabın adresini doğru bir şekilde türetebilir ve doğrulayabilirsiniz.

owner kısıtlamasını uygulamak için, hesabı sahiplenen programın genel anahtarına erişiminiz olmalıdır. Bu, ya ek bir hesap olarak sağlanabilir ya da programınız içinde genel anahtarı sabit kodlayarak yapılabilir.

use anchor_lang::prelude::*;

declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws");

#[program]
pub mod owner_check {
use super::*;
...
pub fn admin_instruction(ctx: Context<Checked>) -> Result<()> {
msg!("Admin: {}", ctx.accounts.admin_config.admin.to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct Checked<'info> {
#[account(
has_one = admin,
)]
admin_config: Account<'info, AdminConfig>,
admin: Signer<'info>,
#[account(
seeds = b"test-seed",
bump,
owner = token_program.key()
)]
pda_derived_from_another_program: AccountInfo<'info>,
token_program: Program<'info, Token>
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}

Laboratuvar

Bu laboratuvarda, sahip kontrolü olmadan kötü niyetli bir aktörün basitleştirilmiş bir token cüzdanından tokenları nasıl boşaltabileceğini göstereceğiz. Bu, İmzacı Yetkilendirme dersi ile benzer bir laboratuvardır.

Bunu göstermek için iki program kullanacağız:

  1. Bir program, tokenları geri çektiği cüzdan hesabında bir sahip kontrolüne sahip değildir.
  2. İkinci program, ilk programın cüzdan hesabını taklit etmek için kötü niyetli bir kullanıcı tarafından oluşturulmuş bir kopyadır.

Sahip kontrolü olmaksızın, kötü niyetli kullanıcı, sahte bir program tarafından sahiplenilen kendi cüzdan hesabını geçerek ilk programın hala işlemi gerçekleştirmesini sağlar.

1. Başlangıç

starter dalından başlangıç kodunu indirmeye başlayın. Başlangıç kodu iki program içerir: clone ve owner_check, ve test dosyası için kurulum içerir.

owner_check programı, iki talimat işleyicisi içerir:

  • initialize_vault: Bir token hesabının ve bir otorite hesabının adreslerini depolayan basit bir cüzdan hesabını başlatır.
  • insecure_withdraw: Token cüzdanından tokenları geri çeker, ancak cüzdan hesabı için bir sahip kontrolüne sahip değildir.
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount};

declare_id!("3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9");

pub const DISCRIMINATOR_SIZE: usize = 8;

#[program]
pub mod owner_check {
use super::*;

pub fn initialize_vault(ctx: Context<InitializeVault>) -> Result<()> {
ctx.accounts.vault.token_account = ctx.accounts.token_account.key();
ctx.accounts.vault.authority = ctx.accounts.authority.key();
Ok(())
}

pub fn insecure_withdraw(ctx: Context<InsecureWithdraw>) -> Result<()> {
let account_data = ctx.accounts.vault.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
let account_state = Vault::try_deserialize(&mut account_data_slice)?;

if account_state.authority != ctx.accounts.authority.key() {
return Err(ProgramError::InvalidArgument.into());
}

let amount = ctx.accounts.token_account.amount;

let seeds = &[
b"token".as_ref(),
&[ctx.bumps.token_account],
];
let signer = [&seeds[..]];

let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.token_account.to_account_info(),
to: ctx.accounts.withdraw_destination.to_account_info(),
},
&signer,
);

token::transfer(cpi_ctx, amount)?;
Ok(())
}
}

#[derive(Accounts)]
pub struct InitializeVault<'info> {
#[account(
init,
payer = authority,
space = DISCRIMINATOR_SIZE + Vault::INIT_SPACE,
)]
pub vault: Account<'info, Vault>,
#[account(
init,
payer = authority,
token::mint = mint,
token::authority = token_account,
seeds = [b"token"],
bump,
)]
pub token_account: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct InsecureWithdraw<'info> {
/// CHECK: Bu hesap Anchor tarafından kontrol edilmeyecek
pub vault: UncheckedAccount<'info>,
#[account(
mut,
seeds = [b"token"],
bump,
)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub withdraw_destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub authority: Signer<'info>,
}

#[account]
#[derive(Default, InitSpace)]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

clone programı tek bir talimat işleyicisi içerir:

  • initialize_vault: Kötü niyetli kullanıcının kendi otoritesini ayarlamasına imkan tanıyan, owner_check programının cüzdan hesabını taklit eden sahte bir cüzdan hesabını başlatır.
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

declare_id!("2Gn5MFGMvRjd548z6vhreh84UiL7L5TFzV5kKGmk4Fga");

pub const DISCRIMINATOR_SIZE: usize = 8;

#[program]
pub mod clone {
use super::*;

pub fn initialize_vault(ctx: Context<InitializeVault>) -> Result<()> {
ctx.accounts.vault.token_account = ctx.accounts.token_account.key();
ctx.accounts.vault.authority = ctx.accounts.authority.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct InitializeVault<'info> {
#[account(
init,
payer = authority,
space = DISCRIMINATOR_SIZE + Vault::INIT_SPACE,
)]
pub vault: Account<'info, Vault>,
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[account]
#[derive(Default, InitSpace)]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

2. Güvensiz geri çekme talimat işleyicisini test edin

Test dosyası, her iki programda da bir cüzdanı başlatma testlerini içerir. insecure_withdraw talimat işleyicisinde, bir sahip kontrolü olmamasının token geri çekme imkanı sağladığını göstermek için bir test ekleyeceğiz.

describe("Sahip Kontrolü", () => {
...
it("güvensiz geri çekme yapıyor", async () => {
try {
const transaction = await program.methods
.insecureWithdraw()
.accounts({
vault: vaultCloneAccount.publicKey,
tokenAccount: tokenPDA,
withdrawDestination: unauthorizedWithdrawDestination,
authority: unauthorizedWallet.publicKey,
})
.transaction();

await anchor.web3.sendAndConfirmTransaction(connection, transaction, [
unauthorizedWallet,
]);

const tokenAccountInfo = await getAccount(connection, tokenPDA);
expect(Number(tokenAccountInfo.amount)).to.equal(0);
} catch (error) {
console.error("Güvensiz geri çekme başarısız oldu:", error);
throw error;
}
});
})

anchor test komutunu çalıştırarak, insecure_withdraw işleminin başarılı bir şekilde tamamlandığını doğrulayın.

owner-check
✔ cüzdanı başlatır (866ms)
✔ sahte cüzdanı başlatır (443ms)
✔ güvensiz geri çekmeyi gerçekleştirir (444ms)

Not: vaultCloneAccount vaultCloneAccount, her iki programın da aynı ayrımcıyı kullanması nedeniyle başarılı bir şekilde ayrıştırılır; bu ayrımcı, aynı Vault yapı adından türetilmiştir.

#[account]
#[derive(Default, InitSpace)]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

3. Güvenli geri çekme talimat işleyicisini ekleyin

Şimdi, Account türü ile bir secure_withdraw talimat işleyicisi ekleyerek güvenlik açığını kapatacağız. Bu, bir sahip kontrolünün gerçekleştirilmesini sağlar.

owner_check programının lib.rs dosyasına, secure_withdraw talimat işleyicisini ve bir SecureWithdraw hesaplar yapısını ekleyin. has_one kısıtlaması, talimat işleyicisine geçirilen token_account ve authority'nin, vault hesabında saklanan değerlerle eşleşmesini sağlamak için kullanılacaktır.

#[program]
pub mod owner_check {
use super::*;
...

pub fn secure_withdraw(ctx: Context<SecureWithdraw>) -> Result<()> {
let amount = ctx.accounts.token_account.amount;

let seeds = &[
b"token".as_ref(),
&[*ctx.bumps.get("token_account").unwrap()],
];
let signer = [&seeds[..]];

let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.token_account.to_account_info(),
to: ctx.accounts.withdraw_destination.to_account_info(),
},
&signer,
);

token::transfer(cpi_ctx, amount)?;
Ok(())
}
}
...

#[derive(Accounts)]
pub struct SecureWithdraw<'info> {
#[account(
has_one = token_account,
has_one = authority
)]
pub vault: Account<'info, Vault>,
#[account(
mut,
seeds = [b"token"],
bump,
)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub withdraw_destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub authority: Signer<'info>,
}

4. Güvenli geri çekme talimat işleyicisini test edin

secure_withdraw talimat işleyicisini test etmek için, önce vaultCloneAccount hesabını kullanarak bir kez çağıracağız ve bunun başarısız olmasını bekleyeceğiz. Sonra, doğru vaultAccount hesabı ile talimat işleyicisini invoke ederek, çalıştığını doğrulamak için talimat işleyicisini çağıracağız.

describe("Sahip Kontrolü", () => {
...
it("hatalı otorite ile güvenli geri çekmeyi başarısız kılar", async () => {
try {
const transaction = await program.methods
.secureWithdraw()
.accounts({
vault: vaultCloneAccount.publicKey,
tokenAccount: tokenPDA,
withdrawDestination: unauthorizedWithdrawDestination,
authority: unauthorizedWallet.publicKey,
})
.transaction();

await anchor.web3.sendAndConfirmTransaction(connection, transaction, [
unauthorizedWallet,
]);
throw new Error("İşlemin başarısız olmasını bekliyordum, ancak başarılı oldu");
} catch (error) {
expect(error).to.be.an("error");
console.log("Hata mesajı:", error.message);
}
});

it("güvenli geri çekmeyi başarıyla gerçekleştirir", async () => {
try {
await mintTo(
connection,
walletAuthority.payer,
tokenMint,
tokenPDA,
walletAuthority.payer,
INITIAL_TOKEN_AMOUNT
);

await program.methods
.secureWithdraw()
.accounts({
vault: vaultAccount.publicKey,
tokenAccount: tokenPDA,
withdrawDestination: authorizedWithdrawDestination,
authority: walletAuthority.publicKey,
})
.rpc();

const tokenAccountInfo = await getAccount(connection, tokenPDA);
expect(Number(tokenAccountInfo.amount)).to.equal(0);
} catch (error) {
console.error("Güvenli geri çekme başarısız oldu:", error);
throw error;
}
});
})

anchor test çalıştırıldığında, vaultCloneAccount hesabı ile yapılan işlemin başarısız olduğunu ve vaultAccount hesabı ile yapılan işlemin başarılı bir şekilde geri çekildiğini göreceksiniz.

"Program 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9 invoke [1]",
"Program log: Talimat: SecureWithdraw",
"Program log: Hesap: vault tarafından tetiklenen AnchorError. Hata Kodu: AccountOwnedByWrongProgram. Hata Numarası: 3007. Hata Mesajı: Verilen hesap, beklenenden farklı bir program tarafından sahiplenilmektedir.",
"Program log: Sol:",
"Program log: 2Gn5MFGMvRjd548z6vhreh84UiL7L5TFzV5kKGmk4Fga",
"Program log: Sağ:",
"Program log: 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9",
"Program 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9 200000 hesaplama biriminin 4449'unu harcadı",
"Program 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9 başarısız oldu: özel program hatası: 0xbbf"

Burada, Anchor'ın Account türünün, hesap doğrulama sürecini sahiplik kontrolleri ile otomatikleştirdiğini görebiliriz. Ayrıca, Anchor hataları, hatanın nedenini belirleyen bilgileri sağlamakta; örneğin, log AnchorError caused by account: vault göstermektedir; bu da hata ayıklamaya yardımcı olur.

✔ hatalı otorite ile güvenli geri çekmeyi başarısız kılar
✔ güvenli geri çekmeyi başarıyla gerçekleştirir (847ms)

Sahiplik kontrollerinin sağlanması, güvenlik açıklarından kaçınmak için kritik önem taşır. Bu örnek, uygun doğrulamanın ne kadar basit olduğunu gösterirken, belirli programların hangi hesaplar tarafından sahiplenildiğini her zaman doğrulamanın önemli olduğunu göstermektedir.

Son çözüm kodunu incelemek isterseniz, repo'nun solution dalında bulunmaktadır.

Mücadele

Bu ünitedeki diğer derslerde olduğu gibi, kendi veya başka programlarınızı denetleyerek güvenlik açığı önlemeyi pratik yapın.

ipucu

Hesaplarda mülkiyet kontrollerinin uygun şekilde uygulandığını doğrulamak için en az bir programı gözden geçirmek için zaman ayırın.

Başka bir programda bir hata veya açık bulursanız, geliştiriciye bildirin. Kendi programınızda bir tane bulursanız, hemen düzeltin.

Laboratuvarı tamamladınız mı?

Kodunuzu GitHub’a yükleyin ve
bize bu ders hakkında ne düşündüğünüzü söyleyin!

tehlike

Unutmayın ki, güvenlik açıkları yalnızca geliştirme aşamasında değil, dağıtım ve kullanım aşamalarında da ortaya çıkabilir.

bilgi

Mülkiyet kontrollerini her zaman güncel tutmak, yazılımınızın güvenliği için kritik öneme sahiptir.

“Güvenlik açığı tespiti geliştirilmiş bir yazılımın temel özelliklerinden biridir.”
— Bilgi Güvenliği Uzmanı


Kodunuzu yönetirken, bu önerilere dikkat etmeyi unutmayın.